#include <strsafe.h>

#include "log.h"

#define MAX_BACKUP_FILES 100

LogException::LogException(std::string msg)
    : m_msg(std::move(msg)) {}

LogException::LogException(const std::string& msg, int lastErrno)
{
    m_msg = std::system_error(std::error_code(lastErrno, std::generic_category()), msg).what();
}

const char* LogException::what() const noexcept { return m_msg.c_str(); }


static inline void ThrowLogException(const std::string& msg, int lastErrno)
{
    LOG_THROW(LogException(msg, lastErrno));
}

static inline void ThrowLogException(std::string msg) { LOG_THROW(LogException(std::move(msg))); }

void WideStrToUTF8(const std::wstring& wstr, std::string& target, BOOL includeTerminateNullChar)
{
    size_t wstrSize = wstr.size();
    if (wstrSize == 0) {
        target.resize(0);
        return;
    }

    // Ensure the UTF-16 string size does not exceed the maximum limit for conversion to UTF-8.
    // Subtracting 1 provides an additional safety margin to prevent overflow.
    // Each UTF-16 character can be up to 4 UTF-8 bytes, so we divide the maximum int value by 4.
    if (wstrSize > static_cast<size_t>((std::numeric_limits<int>::max)()) / 4 - 1) {
        ThrowLogException("UTF-16 string is too big to be converted to UTF-8");
    }

    int resultSize = WideCharToMultiByte(CP_UTF8, 0, wstr.data(),
                                         includeTerminateNullChar ? -1 : static_cast<int>(wstrSize),
                                         NULL, 0, NULL, NULL);
    if (resultSize > 0) {
        target.resize(resultSize);
        resultSize = WideCharToMultiByte(CP_UTF8, 0, wstr.data(),
                                         includeTerminateNullChar ? -1 : static_cast<int>(wstrSize),
                                         target.data(), resultSize, NULL, NULL);
        if (resultSize > 0) {
            return;
        }
    }
    ThrowLogException("WideCharToMultiByte failed. Last error: " + std::to_string(GetLastError()));
}

static inline std::string GetLocalTime()
{
    SYSTEMTIME st;
    GetLocalTime(&st);

    return std::format("[{:04d}-{:02d}-{:02d} {:02d}:{:02d}:{:02d}.{:03d}]",
                       st.wYear, st.wMonth, st.wDay,
                       st.wHour, st.wMinute, st.wSecond, st.wMilliseconds);
}

static const char* BaseName(const char* fileName)
{
    // If the size is 2 (1 character + null terminator), We can use the more efficient strrchr.
    if (sizeof(FOLDER_SPES) == 2) {
        const char* rv = std::strrchr(fileName, FOLDER_SPES[0]);
        return rv != nullptr ? rv + 1 : fileName;
    } else {
        const std::reverse_iterator<const char*> begin(fileName + std::strlen(fileName));
        const std::reverse_iterator<const char*> end(fileName);

        const auto it = std::find_first_of(begin, end, std::begin(FOLDER_SPES),
                                           std::end(FOLDER_SPES) - 1);
        return it != end ? it.base() : fileName;
    }
}

// Return directory name from given path
static inline std::string DirName(const std::string& path)
{
    size_t pos = path.find_last_of(LOG_FOLDER_SEPS);
    return pos != std::string::npos ? path.substr(0, pos) : std::string{};
}

// Return true if path exists (file or directory)
static inline BOOL PathExists(const std::string& fileName) noexcept
{
    struct _stat buffer;
    return (_stat(fileName.c_str(), &buffer) == 0);
}

static inline size_t FileSize(FILE* f)
{
    if (f == nullptr) {
        ThrowLogException("Failed to get file size, fd is nullptr");
    }

    int fd = _fileno(f);
#ifdef _WIN64
    int64_t ret = _filelengthi64(fd);
    if (ret >= 0) {
        return static_cast<size_t>(ret);
    }
#else
    int32_t ret = _filelength(fd);
    if (ret >= 0) {
        return static_cast<size_t>(ret);
    }
#endif
    return 0;
}

static inline BOOL OpenFile(FILE** fp, const std::string& fileName, const std::string& mode)
{
    *fp = _fsopen(fileName.c_str(), mode.c_str(), _SH_DENYNO);
    return *fp == nullptr;
}

// return true on success
static inline BOOL MkDir(const std::string& path)
{
    return _mkdir(path.c_str()) == 0;
}

// create the given directory - and all directories leading to it
// return true on success or if the directory already exists
static BOOL CreateDir(const std::string& path)
{
    if (PathExists(path)) {
        return true;
    }

    if (path.empty()) {
        return false;
    }

    size_t offSet = 0;
    do {
        size_t tokenPos = path.find_first_of(LOG_FOLDER_SEPS, offSet);
        // treat the entire path as a folder if no folder separator not found
        if (tokenPos == std::string::npos) {
            tokenPos = path.size();
        }

        std::string subdir = path.substr(0, tokenPos);
        // If subdir is just a drive letter, add a slash e.g. "d:"=>"d:\",
        // otherwise PathExists(subdir) returns FALSE.
        const BOOL isDrive = subdir.length() == 2 && subdir[1] == ':';
        if (isDrive) {
            subdir += '\\';
            tokenPos++;
        }

        if (!subdir.empty() && !PathExists(subdir) && !MkDir(subdir)) {
            return false;  // return error if failed creating dir
        }
        offSet = tokenPos + 1;
    } while (offSet < path.size());

    return true;
}

static inline int Remove(const std::string& fileName) noexcept
{
    return std::remove(fileName.c_str());
}

static inline int Rename(const std::string& filename1, const std::string& filename2) noexcept
{
    return std::rename(filename1.c_str(), filename2.c_str());
}

EventLog::~EventLog()
{
    if (m_hEventLog) {
        DeregisterEventSource(m_hEventLog);
        m_hEventLog = nullptr;
    }
}

HANDLE EventLog::GetEventLogHandle()
{
    if (!m_hEventLog) {
        m_hEventLog = RegisterEventSourceA(nullptr, m_source.c_str());
        if (!m_hEventLog || m_hEventLog == (HANDLE)ERROR_ACCESS_DENIED) {
            ThrowLogException("RegisterEventSourceA failed. Last error: " + std::to_string(GetLastError()));
        }
    }

    return m_hEventLog;
}

WORD EventLog::GetEventType(UINT level)
{
    switch (level) {
        case LOG_LEVEL_DEBUG:
            return EVENTLOG_SUCCESS;
        case LOG_LEVEL_INFO:
            return EVENTLOG_INFORMATION_TYPE;
        case LOG_LEVEL_WARN:
            return EVENTLOG_WARNING_TYPE;
        case LOG_LEVEL_ERROR:
            return EVENTLOG_ERROR_TYPE;
        default:
            return EVENTLOG_INFORMATION_TYPE;
    }
}

void EventLog::WriteToEventLog(const std::string& msg, UINT level)
{
    std::lock_guard<std::mutex> guard(m_mutex);
    LPCSTR lpstr = msg.data();
    BOOL succeeded = ReportEventA(GetEventLogHandle(), GetEventType(level),
                                  (WORD)level, m_eventId, nullptr,
                                  1, 0, &lpstr, nullptr);
    if (!succeeded) {
        ThrowLogException("ReportEvent failed. Last error: " + std::to_string(GetLastError()));
    }
}

FileMgr::~FileMgr()
{
    Close();
}

BOOL FileMgr::Open(const std::string& fileName, BOOL truncate)
{
    Close();
    m_fileName = fileName;

    for (UINT tries = 0; tries < m_openTries; ++tries) {
        // create containing folder if not exists already.
        CreateDir(DirName(fileName));
        if (truncate) {
            std::FILE* tmp;
            if (OpenFile(&tmp, fileName, "wb")) {
                continue;
            }
            std::fclose(tmp);
        }
        if (!OpenFile(&m_fd, fileName, "ab")) {
            return TRUE;
        }
        Sleep(m_openInterval);
    }

    WriteToEventLog(std::format("Failed to open file: {}, with error {}", m_fileName, errno), LOG_LEVEL_ERROR);
    return FALSE;
}

BOOL FileMgr::Reopen(BOOL truncate)
{
    if (m_fileName.empty()) {
        WriteToEventLog("Failed to reopen file", LOG_LEVEL_ERROR);
        return FALSE;
    }

    return Open(m_fileName, truncate);
}

BOOL FileMgr::Flush()
{
    if (m_fd == nullptr) {
        WriteToEventLog("m_fd is nullptr", LOG_LEVEL_ERROR);
        return FALSE;
    }

    if (std::fflush(m_fd) != 0) {
        WriteToEventLog(std::format("Failed to flush file: {}, with error: {}", m_fileName, errno), LOG_LEVEL_ERROR);
        return FALSE;
    }

    return TRUE;
}

BOOL FileMgr::Sync()
{
    if (m_fd == nullptr) {
        WriteToEventLog("m_fd is nullptr", LOG_LEVEL_ERROR);
        return FALSE;
    }

    if (FlushFileBuffers(reinterpret_cast<HANDLE>(_get_osfhandle(_fileno(m_fd)))) == 0) {
        WriteToEventLog(std::format("Failed to fsync file: {}, with error: {}", m_fileName, errno), LOG_LEVEL_ERROR);
        return FALSE;
    }

    return TRUE;
}

void FileMgr::Close()
{
    if (m_fd != nullptr) {
        std::fclose(m_fd);
        m_fd = nullptr;
    }
}

BOOL FileMgr::Write(const std::string& buf)
{
    if (m_fd == nullptr) {
        WriteToEventLog("m_fd is nullptr", LOG_LEVEL_ERROR);
        return FALSE;
    }

    size_t msgSize = buf.size();
    if (std::fwrite(buf.data(), 1, msgSize, m_fd) != msgSize) {
        WriteToEventLog(std::format("Failed to write file: {}, with error: {}", m_fileName, errno), LOG_LEVEL_ERROR);
        return FALSE;
    }

    return TRUE;
}

BOOL FileMgr::Size(size_t& size)
{
    if (m_fd == nullptr) {
        WriteToEventLog(std::format("Cannot use Size() on closed file: {}", m_fileName), LOG_LEVEL_ERROR);
        return FALSE;
    }

    size = FileSize(m_fd);
    return TRUE;
}

const std::string& FileMgr::FileName() const
{
    return m_fileName;
}

std::tuple<std::string, std::string> FileMgr::SplitByExtension(const std::string& fileName)
{
    size_t extIndex = fileName.rfind('.');
    // no valid extension found - return whole path and empty string as extension
    if (extIndex == std::string::npos || extIndex == 0 || extIndex == fileName.size() - 1) {
        return std::make_tuple(fileName, std::string());
    }

    // treat cases like "/etc/rc.d/somelogfile or "/abc/.hiddenfile"
    size_t folderIndex = fileName.find_last_of(LOG_FOLDER_SEPS);
    if (folderIndex != std::string::npos && folderIndex >= extIndex - 1) {
        return std::make_tuple(fileName, std::string());
    }

    // finally - return a valid base and extension tuple
    return std::make_tuple(fileName.substr(0, extIndex), fileName.substr(extIndex));
}

void FileMgr::WriteToEventLog(const std::string& msg, UINT level)
{
    m_eventLog.WriteToEventLog(msg, level);
}

DWORD Log::Init(std::string fileName, size_t maxSize, size_t backupFiles)
{
    if (m_bInitialised) {
        return ERROR_ALREADY_INITIALIZED;
    }

    do {
        if (maxSize == 0) {
            m_fileMgr.WriteToEventLog("maxSize arg cannot be zero", LOG_LEVEL_ERROR);
            break;
        }
        m_maxSize = maxSize;

        if (backupFiles > MAX_BACKUP_FILES) {
            m_fileMgr.WriteToEventLog(std::format("backupFiles arg cannot exceed {}", MAX_BACKUP_FILES),
                                      LOG_LEVEL_ERROR);
            break;
        }
        m_maxBackupFiles = backupFiles;

        m_fileName = std::move(fileName);
        if (!m_fileMgr.Open(CalcFileName(m_fileName, 0))) {
            break;
        }

        // Called only once
        if (!m_fileMgr.Size(m_currentSize)) {
            break;
        }

        m_bInitialised = TRUE;
        return ERROR_SUCCESS;
    } while (0);

    return ERROR_FUNCTION_FAILED;
}

std::string Log::CalcFileName(const std::string& fileName, size_t index)
{
    if (index == 0u) {
        return fileName;
    }

    std::string baseName;
    std::string extension;
    std::tie(baseName, extension) = m_fileMgr.SplitByExtension(fileName);
    return std::format("{}.{}{}", baseName, index, extension);
}

BOOL Log::RenameFile(const std::string& srcFileName, const std::string& targetFileName)
{
    Remove(targetFileName);
    return Rename(srcFileName, targetFileName) == 0;
}

BOOL Log::RotateFile()
{
    m_fileMgr.Close();
    for (size_t i = m_maxBackupFiles; i > 0; --i) {
        std::string src = CalcFileName(m_fileName, i - 1);
        if (!PathExists(src)) {
            continue;
        }

        std::string target = CalcFileName(m_fileName, i);
        if (!RenameFile(src, target)) {
            // If failed try again after 100ms delay.
            Sleep(100);
            if (!RenameFile(src, target)) {
                // truncate the log file.
                m_fileMgr.Reopen(TRUE);
                m_fileMgr.WriteToEventLog(
                    std::format("Failed to rotate file, rename {} to {}, with error: {}", src, target, errno),
                    LOG_LEVEL_ERROR);
            }
        }
    }

    return m_fileMgr.Reopen(TRUE);
}

void Log::LogToFile(UINT level, const char* fileNameIn, int lineIn, const std::string& msg)
{
    if (!m_bInitialised) {
        return;
    }

    std::lock_guard<std::mutex> guard(m_mutex);

    if (!m_enableLog) {
        return;
    }

    std::string logEntry = std::format("{}[{}][{}:{}]{}\r\n", GetLocalTime(),
                                       LOG_SHORT_LEVEL_NAMES[level],
                                       BaseName(fileNameIn), lineIn, msg);

    size_t newSize = m_currentSize + logEntry.size();
    // Rotate file if the new estimated file size exceeds max size.
    // Only check the real size when newSize > m_maxSize.
    if (newSize > m_maxSize) {
        if (!m_fileMgr.Flush()) {
            goto error;
        }

        size_t size = 0;
        if (!m_fileMgr.Size(size)) {
            goto error;
        }

        if (size > 0) {
            if (!RotateFile()) {
                goto error;
            }
            newSize = logEntry.size();
        }
    }

    if (!m_fileMgr.Write(logEntry) || !m_fileMgr.Flush()) {
        goto error;
    }

    m_currentSize = newSize;
    return;

error:
    m_enableLog = FALSE;
}

void Log::LogToFile(UINT level, const char* fileNameIn, int lineIn, const std::wstring& msg)
{
    if (!m_bInitialised) {
        return;
    }

    std::string buf;
    WideStrToUTF8(msg, buf);
    LogToFile(level, fileNameIn, lineIn, buf);
}
